﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Numerics;
using System.ComponentModel;
using System.Threading;
using ArgusLight.Threading.SynchronizedObjects;

namespace ArgusLight.Fractals2D
{
	/// <summary>
	/// Base class for fractals based on complex-number-operations like the
	/// Mandelbrot and the Julia Set.
	/// </summary>
	public abstract class ComplexFractal : Fractal
	{
		#region Fields
		protected double limit = 2;
		private bool[] workDone;
		private const int SleepMs = 200;
		private SynchronizedStack<List<Coord>> coordStack;
		private Count pixelCount;
		private int numberOfThreads = 8;
		private bool[] threadFinished;
		private static int listSize = 100;
		#endregion

		#region Properties
		public new ComplexFractalParameter Parameter { get { return this.parameter.GetCopy<ComplexFractalParameter>(); } }
		protected ComplexFractalParameter para { get { return (ComplexFractalParameter)this.parameter; } }
		public new ComplexFractalColoring.Coloring Coloring { get { return (ComplexFractalColoring.Coloring)this.coloring; } }
		//protected double ZoomFactor { get { return this.startArea.Width/this.para.ShownArea.Width; } }
		public override Type ColoringBase{get { return typeof(ComplexFractalColoring.Coloring); }}
		public int NumberOfThreads
		{
			get { return this.numberOfThreads; }
			set { this.numberOfThreads = Math.Max(1, value); }
		}
		#endregion

		#region Constructor
		public ComplexFractal(int Width, int Height, int Iterations, ComplexFractalColoring.Coloring coloring)
			: base(Width, Height, coloring)
		{
			ComplexFractalParameter para = this.parameter.GetCopy<ComplexFractalParameter>();
			para.Iterations = Iterations;
			para.ShownArea = (new Rect(-2, -2, 4, 4)).ChangeRatio(this.GetRatio(), System.Windows.ExtensionMethods.ChangeRatioOptions.IncreaseAreaSize, System.Windows.ExtensionMethods.RectOption.Center);
			this.parameter = para;
		}

		public ComplexFractal()
			: base()
		{
		}
		#endregion

		#region Methods
		public override void SetColoring(Coloring c)
		{
			if ((c is ComplexFractalColoring.Coloring) == false)
				throw new ArgumentException("Non Compatible coloring", "c");

			if (this.coloring != null)
				ColoringConverter.TryConversion(ref this.coloring, ref c);
			this.coloring = c;
		}

		public static Rect ScreenToComplex(Rect PixelArea, Rect ShownArea, Rect ZoomArea, double Rotation)
		{
			Point TopLeft = ComplexFractal.ScreenToComplex(ZoomArea.GetTopLeft(), PixelArea, ShownArea, Rotation).ToPoint();
			Point BottomRight = ComplexFractal.ScreenToComplex(ZoomArea.GetBottomRight(), PixelArea, ShownArea, Rotation).ToPoint();
			return new Rect(TopLeft, BottomRight);
		}

		public static Complex ScreenToComplex(Point Screen, Rect PixelArea, Rect ShownArea, double Rotation)
		{
			double x = Screen.X - PixelArea.Width / 2;
			double y = PixelArea.Height / 2 - Screen.Y;
			double re = x * ShownArea.Width / PixelArea.Width;
			double im = y * ShownArea.Height / PixelArea.Height;
			Complex rot = Complex.FromPolarCoordinates(1, -Rotation);
			return (new Complex(re, im)) * rot + ShownArea.GetCenter().ToComplex();
		}

		public Complex ScreenToComplex(Point Screen)
		{
			Rect Pixel = new Rect(0, 0, this.Width, this.Height);
			return ComplexFractal.ScreenToComplex(Screen, Pixel, this.para.ShownArea, this.parameter.Rotation);
		}

		/*
		public Complex ScreenToComplex(Point ScreenCoord)
		{
			Complex rotation = Complex.FromPolarCoordinates(1, this.Parameter.Rotation);
			Double x = ((ScreenCoord.X - this.Width / 2) * this.para.ShownArea.Width / this.Width);
			Double y = ((this.Height / 2 - ScreenCoord.Y) * this.para.ShownArea.Height / this.Height);
			return new Complex(x, y) * rotation + this.para.ShownArea.GetCenter().ToComplex();
		}
		public static Complex ScreenToComplex(Point ScreenCoord, Size MSize, Size MArea, double Rotation, Complex Center)
		{
			Double x = ((ScreenCoord.X - MSize.Width / 2) * MArea.Width / MSize.Width);
			Double y = ((MSize.Height / 2 - ScreenCoord.Y) * MArea.Height / MSize.Height);
			return new Complex(x, y) * Complex.FromPolarCoordinates(1, Rotation) + Center;
		}
		public Size ScreenToComplex(Size s)
		{
			Double w = s.Width * this.para.ShownArea.Width / this.Width;
			Double h = s.Height * this.para.ShownArea.Height / this.Height;
			return new Size(w, h);
		}
		public Point ComplexToScreen(Complex MandelbrotCoord)
		{
			Complex rotation = Complex.FromPolarCoordinates(1, this.Parameter.Rotation);
			MandelbrotCoord -= this.para.ShownArea.GetCenter().ToComplex();
			MandelbrotCoord /= rotation;
			Double x = (MandelbrotCoord.Real * this.Width / this.para.ShownArea.Width) + this.Width / 2;
			Double y = (MandelbrotCoord.Imaginary * this.Height / this.para.ShownArea.Height) - this.Height / 2;
			return new Point(x, -y);
		}
		*/

		protected override void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
		{
#if false
			BackgroundWorker worker = (BackgroundWorker)sender;
			ComplexFractalParameter para = ((FractalParameter)e.Argument).GetCopy<ComplexFractalParameter>();
			para.ShownArea = para.ShownArea.ChangeRatio((new Size(para.PixelWidth, para.PixelHeight)).GetRatio(), System.Windows.ExtensionMethods.ChangeRatioOptions.IncreaseAreaSize, System.Windows.ExtensionMethods.RectOption.Center);
			e.Result = para;

			if (worker.CancellationPending == true)
			{
				e.Cancel = true;
				return;
			}

			Size size = new Size(para.PixelWidth, para.PixelHeight);

			this.workDone = new bool[para.PixelHeight];

			for (int y = 0; y < para.PixelHeight; y++)
			{
				if (worker.CancellationPending == true)
				{
					e.Cancel = true;
					return;
				}

				object[] args = new object[]
				{
					y,
					para,
					worker,
					e
				};
				ThreadPool.QueueUserWorkItem(this.calculateLine, args);
			}

			int max = 0;
			while (true)
			{
				if (worker.CancellationPending == true)
				{
					e.Cancel = true;
					return;
				}

				int finished = 0;
				for (int i = 0; i < this.workDone.Length; i++)
				{
					if (this.workDone[i] == true)
						finished++;
				}
				if (finished > max)
				{
					worker.ReportProgress(finished * 100 / this.workDone.Length);
					max = finished;
				}
				Thread.Sleep(SleepMs);

				if (finished == this.workDone.Length)
					break;
			}
			worker.ReportProgress(100);
#else
			ComplexFractalParameter para = this.para;
			para.ShownArea = para.ShownArea.ChangeRatio((new Size(para.PixelWidth, para.PixelHeight)).GetRatio(), System.Windows.ExtensionMethods.ChangeRatioOptions.IncreaseAreaSize, System.Windows.ExtensionMethods.RectOption.Center);
			this.coordStack = new SynchronizedStack<List<Coord>>();
			List<Coord> borderCoords = new List<Coord>(listSize+10);
			for (int x = 0; x < para.PixelWidth; x++)
			{
				borderCoords.Add(new Coord(x, 0));
				borderCoords.Add(new Coord(x, para.PixelHeight - 1));
				if (borderCoords.Count > listSize)
				{
					this.coordStack.Push(borderCoords);
					borderCoords = new List<Coord>(listSize + 10);
				}
			}
			for (int y = 0; y < para.PixelHeight; y++)
			{
				borderCoords.Add(new Coord(0, y));
				borderCoords.Add(new Coord(para.PixelWidth - 1, y));
				if (borderCoords.Count > listSize)
				{
					this.coordStack.Push(borderCoords);
					borderCoords = new List<Coord>(listSize + 10);
				}
			}
			this.coordStack.Push(borderCoords);
			this.pixelCount = new Count(this.coordStack.Count);
			this.threadFinished = new bool[this.numberOfThreads];
			
			for (int i = 0; i < this.numberOfThreads; i++)
			{
				object[] args = new object[] { e, i };
				this.threadFinished[i] = false;
				ThreadPool.QueueUserWorkItem(this.calculationThreadMethod, args);
			}

			bool finished = false;
			while (finished == false)
			{
				Thread.Sleep(SleepMs);
				finished = true;
				for (int i = 0; i < this.threadFinished.Length; i++)
				{
					if (this.threadFinished[i] == false)
					{
						finished = false;
						break;
					}
				}
				int pCount = this.pixelCount.Value;
				int stackCount = this.coordStack.Count;
				int finishedCount = pCount - stackCount;
				int percentage = 100*finishedCount / pCount;
				this.backgroundWorker.ReportProgress(percentage, new int[]{stackCount * listSize, finishedCount*listSize});
			}
			//this.backgroundWorker.ReportProgress(100);
#endif
		}

#if false
		private void calculateLine(object state)
		{
			object[] args = (object[])state;
			int y = (int)args[0];
			ComplexFractalParameter para = this.para;
			BackgroundWorker worker = this.backgroundWorker;
			DoWorkEventArgs e = (DoWorkEventArgs)args[1];

			Rect pixelArea = new Rect(0, 0, para.PixelWidth, para.PixelHeight);
			for (int x = 0; x < para.PixelWidth; x++)
			{
				Complex z = Complex.Zero;
				Complex c = ComplexFractal.ScreenToComplex(new Point(x, y), pixelArea, para.ShownArea, para.Rotation);
				this.Coloring.z_set(x, y, z);
				this.Coloring.c_set(x, y, c);
				bool b = false;

				for (int i = 0; i < para.Iterations; i++)
				{
					if (worker.CancellationPending == true)
					{
						e.Cancel = true;
						return;
					}

					if (z.Magnitude > this.limit)
					{
						this.Coloring.Iterations_set(x, y, i);
						b = true;
						break;
					}

					z = this.IterationFormula(c, z);
					this.Coloring.z_set(x, y, z);
				}

				if (b == false)
					this.Coloring.Iterations_set(x, y, para.Iterations);
			}
			//worker.ReportProgress((int)(y / (double)para.PixelHeight));
			this.workDone[y] = true;
		}
#endif

		private void calculationThreadMethod(object state)
		{
			object[] args = (object[])state;
			DoWorkEventArgs e = (DoWorkEventArgs)args[0];
			int MaxI = this.para.Iterations;
			ComplexFractalParameter para = this.para;
			int threadNumber = (int)args[1];
			Rect pixelArea = new Rect(0, 0, para.PixelWidth, para.PixelHeight);
			List<Coord> addList = new List<Coord>(listSize+10);
			while (this.coordStack.Count > 0)
			{
				List<Coord> coordList = new List<Coord>();
				if (this.coordStack.TryPop(ref coordList) == false)
					break;

				foreach (Coord a in coordList)
				{
					//Coord a = coordList[j];
					if (this.backgroundWorker.CancellationPending == true)
					{
						e.Cancel = true;
						this.threadFinished[threadNumber] = true;
						return;
					}

					if (this.Coloring.Iterations_get(a.x, a.y) > 0)
						continue;
					if (this.Coloring.z_get(a.x, a.y).Magnitude > this.limit)
						continue;

					Complex z = Complex.Zero;
					Complex c = ComplexFractal.ScreenToComplex(new Point(a.x, a.y), pixelArea, para.ShownArea, para.Rotation);
					this.Coloring.z_set(a.x, a.y, z);
					this.Coloring.c_set(a.x, a.y, c);
					for (int i = 0; i < MaxI; i++)
					{
						if (this.backgroundWorker.CancellationPending == true)
						{
							e.Cancel = true;
							this.threadFinished[threadNumber] = true;
							return;
						}

						this.Coloring.Iterations_set(a.x, a.y, i);
						if (z.Magnitude > this.limit)
							break;
						z = this.IterationFormula(c, z);
						this.Coloring.z_set(a.x, a.y, z);
					}

					if (z.Magnitude > this.limit)
					{
						for (int x = -1; x < 2; x++)
						{
							for (int y = -1; y < 2; y++)
							{
								Coord b = new Coord(a.x + x, a.y + y);
								if (b.x < 0 || b.x >= para.PixelWidth)
									continue;
								if (b.y < 0 || b.y >= para.PixelHeight)
									continue;
								if (this.Coloring.Iterations_get(b.x, b.y) > 0)
									continue;
								if (this.Coloring.z_get(b.x, b.y).Magnitude > this.limit)
									continue;
								addList.Add(b);
							}
						}
					}
					else
					{
						this.Coloring.Iterations_set(a.x, a.y, para.Iterations);
					}
					if (addList.Count > listSize)
					{
						this.coordStack.Push(addList);
						this.pixelCount.Increment();
						addList = new List<Coord>(listSize + 10);
					}
				}
				if (addList.Count > 0)
				{
					this.coordStack.Push(addList);
					this.pixelCount.Increment();
					addList = new List<Coord>(listSize + 10);
				}
			}
			this.threadFinished[threadNumber] = true;
		}

		protected abstract Complex IterationFormula(Complex c, Complex z);

		public bool StartCalculation(ComplexFractalParameter parameter)
		{
			return base.StartCalculation(parameter);
		}
		#endregion

		struct Coord
		{
			public int x;
			public int y;

			public Coord(int x, int y)
			{
				this.x = x;
				this.y = y;
			}
		}
	}
}
